// Copyright 2012 Jay Young. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview
 *
 * A visitor that finds and documents @enums.
 */

var Token = com.google.javascript.rhino.Token;



/**
 * A node visitor that finds and documents @enums.  Currently supports string
 * and number enums.  Other types may work, but behavoir may be undefined and
 * cause errors.
 *
 * @param {boolean=} debug Turns off error checking.  Useful for exploring edge
 *    cases.
 *
 * @constructor
 * @implements {Documenter.Visitor}
 */
var EnumV = exports.EnumV = function EnumV(debug) {
  /**
   * The next visitor in the chain.
   *
   * @type {Documenter.Visitor}
   * @private
   */
  this.nextVisitor_ = null;
  
  /**
   * A map of enum keys to values.
   * @type {Object.<string, *>}
   */
  this.map = {};
  
  /**
   * @type {boolean}
   * @private
   */
  this.DEBUG_ = !!debug;
};


/**
 * @param {Documenter.Visitor} visitor The next visitor in the chain.
 */
EnumV.prototype.setNext = function (visitor) {
  this.nextVisitor_ = visitor;
};


/**
 * Determine if a node represents an assignment to a prototype property and
 * is a function.  If the right-hand value is not a function, this will
 * also check whether it is goog.nullFunction or goog.abstractMethod and will
 * document the assignment as a method if so.
 *
 * @param {Node} node The node to visit.
 * @parma {DocInfo} docInfo The page's documentation info object.
 * @param {JSDocInfo} jsdoc The jsdoc for the given node or null if no
 *    doc comments were attached.
 */
EnumV.prototype.visit = function (node, docInfo, jsdoc) {
  if (jsdoc && jsdoc.hasEnumParameterType()) {

    var type = docInfo.resolveType(jsdoc.getEnumParameterType()),
        name = node.getFirstChild().getQualifiedName(),
        enumDesc = docInfo.getEnum(name);

    enumDesc.enumDoc = jsdoc.getBlockDescription();
    enumDesc.type = type;
    enumDesc.visibility = docInfo.visibility(jsdoc.getVisibility()) || '';

    if (jsdoc.isDeprecated()) {
      enumDesc.deprecationReason = jsdoc.getDeprecationReason() ||
          'No reason given.';
    }

    // Capture @see annotations
    var references = jsdoc.getReferences();
    if (references) {
      var refList = enumDesc.references = [],
          iterator = references.iterator();
      while (iterator.hasNext()) {
        refList.push(iterator.next());
      }
    }
    
    var enumObj = node.getChildAtIndex(1);

    if (enumObj.getType() === Token.OBJECTLIT) {
      var enumNodes = enumObj.children().iterator(),
          map = enumDesc.map = {};
  
      var enumType;
      while (enumNodes.hasNext()) {
        var enumNode = enumNodes.next(),
            key = enumNode.getString(),
            value = enumNode.getFirstChild(),
            enumNodeDoc = enumNode.getJSDocInfo(),
            doc = enumNodeDoc ? enumNodeDoc.getBlockDescription() : '';
  
        if (value.getType() === Token.STRING) {
          if (enumType) {
            this.assertEnumType(enumType, Token.STRING, key);
          }
          else {
            enumType = Token.STRING;
          }
  
          map[key] = {
            doc: doc,
            value: value.getString()
          };
        }
        else if (value.getType() === Token.NUMBER) {
          if (enumType) {
            this.assertEnumType(enumType, Token.NUMBER, key);
          }
          else {
            enumType = Token.NUMBER;
          }
  
          map[key] = {
            doc: doc,
            value: value.getDouble()
          };
        }
        else if (value.getType() === Token.CALL) {
          var func = value.getFirstChild().getQualifiedName();
          if (func === 'goog.events.getUniqueId') {
            if (enumType) {
              this.assertEnumType(enumType, Token.STRING, key);
            }
            else {
              enumType = Token.STRING;
            }
  
            map[key] = {
              doc: doc,
              value: value.getChildAtIndex(1).getString() + '_<unique #>'
            };
          }
          else if (func === 'goog.getCssName') {
            if (enumType) {
              this.assertEnumType(enumType, Token.STRING, key);
            }
            else {
              enumType = Token.STRING;
            }
  
            map[key] = {
              doc: doc,
              value: 'CSS Class Map Key: '
                  + value.getChildAtIndex(1).getString()
            };
          }
        }
        else {
          map[key] = {
            doc: doc,
            value: null
          };
        }
  
        if (! Object.hasOwnProperty.call(map, key)) {
          throw Error('Could not recognize assignment to enum property ' + key);
        }
      }
      
    }
  }

  if (this.nextVisitor_) {
    this.nextVisitor_.visit(node, docInfo, jsdoc);
  }
};


/**
 * Make sure that the value type is the same as the enum type.
 *
 * @param {number} enumType The Token type of the enum.
 * @param {number} valueType The Token type of the value being documented.
 * @param {string} keyName The name of the key being assigned to.
 *
 * @throws {Error} If the types do not match.
 */
EnumV.prototype.assertEnumType = function(enumType, valueType, keyName) {
  if ((! this.DEBUG_) && enumType !== valueType) {
    throw Error('Cannot assign value of type ' + Token.name(valueType) +
                ' to enum of type ' + Token.name(enumType) + ' for key "' +
                keyName + '".');
  }
}